package com.vision.admin.user.cl.ui;

import static com.vision.admin.cl.AdminServices.roleSvc;
import static com.vision.admin.cl.AdminServices.userSvc;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;

import com.allen_sauer.gwt.log.client.Log;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.Editor;
import com.google.gwt.editor.client.SimpleBeanEditorDriver;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiFactory;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Widget;
import com.sencha.gxt.core.client.Style.SelectionMode;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.data.shared.PropertyAccess;
import com.sencha.gxt.data.shared.Store;
import com.sencha.gxt.widget.core.client.TabItemConfig;
import com.sencha.gxt.widget.core.client.Window;
import com.sencha.gxt.widget.core.client.button.TextButton;
import com.sencha.gxt.widget.core.client.container.AbstractHtmlLayoutContainer.HtmlData;
import com.sencha.gxt.widget.core.client.container.HtmlLayoutContainer;
import com.sencha.gxt.widget.core.client.container.VerticalLayoutContainer;
import com.sencha.gxt.widget.core.client.event.HideEvent;
import com.sencha.gxt.widget.core.client.event.HideEvent.HideHandler;
import com.sencha.gxt.widget.core.client.event.SelectEvent;
import com.sencha.gxt.widget.core.client.form.PasswordField;
import com.sencha.gxt.widget.core.client.form.StoreFilterField;
import com.sencha.gxt.widget.core.client.form.TextField;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.Grid;
import com.sencha.gxt.widget.core.client.grid.GridView;
import com.sencha.gxt.widget.core.client.menu.Item;
import com.sencha.gxt.widget.core.client.menu.MenuItem;
import com.sencha.gxt.widget.core.client.selection.SelectionChangedEvent;
import com.sencha.gxt.widget.core.client.selection.SelectionChangedEvent.SelectionChangedHandler;
import com.vision.admin.cl.AdminServices;
import com.vision.admin.cl.AdminTasks;
import com.vision.admin.cl.resource.AdminResources;
import com.vision.admin.cl.ui.component.CompanyGrid;
import com.vision.admin.cl.ui.component.CompanyListWindow;
import com.vision.admin.user.cl.UserProperties;
import com.vision.core.cl.BaseModule;
import com.vision.core.cl.Callback;
import com.vision.core.cl.CoreValidators;
import com.vision.core.cl.event.CreatedEvent;
import com.vision.core.cl.event.CreatedEvent.CreationHandler;
import com.vision.core.cl.event.UserCreatedEvent;
import com.vision.core.cl.resource.CoreResources;
import com.vision.core.cl.ui.ConfirmDialogHandler;
import com.vision.core.cl.ui.TabItemPanel;
import com.vision.core.cl.ui.UI;
import com.vision.core.cl.ui.component.BasicGridView;
import com.vision.core.cl.ui.component.DualGridPanel;
import com.vision.core.cm.db.data.Company;
import com.vision.core.cm.db.data.Role;
import com.vision.core.cm.db.data.User;
import com.vision.core.cm.db.data.User.UserStatus;
import com.vision.core.cm.util.DateUtils;

/**
 * 
 * @author Mark
 * 
 */
public class UsersPanel extends TabItemPanel implements Editor<User> {

	interface Binder extends UiBinder<Widget, UsersPanel> { }

	private static final Binder binder = GWT.create(Binder.class);
	
	@UiField VerticalLayoutContainer mainPanel;
	
	// Ignored by the form driver since these are not fields in the User
	@Ignore @UiField StoreFilterField<User> gridFilter;
	@Ignore @UiField TextButton addBtn;
	@Ignore @UiField MenuItem refreshMenuItem;
	@Ignore @UiField MenuItem deleteMenuItem;
	
	@UiField Grid<User> grid;
	
	// Form fields that are bound by the form driver to the fields in the User class.
	// The name of these form fields must match the name of the field in the User class.
	@UiField TextField username;
	@UiField PasswordField password;
	@UiField TextField email;
	@UiField TextField firstName;
	@UiField TextField middleName;
	@UiField TextField lastName;
	
	// Ignored by the form driver since these are not fields in the User
	@Ignore @UiField TextButton activateBtn;
	@Ignore @UiField TextButton saveBtn;
	@Ignore @UiField TextButton cancelBtn;
	
	@Ignore @UiField TextButton assignCompanyBtn;
	@Ignore @UiField TextButton unassignCompanyBtn;
	
	@Ignore @UiField CompanyGrid companiesGrid;
	
	// Interface used to bind the form fields to the User fields
	interface UserDriver extends SimpleBeanEditorDriver<User, UsersPanel> { }
	
	// Instance of the UserDriver interface
	private UserDriver driver = GWT.create(UserDriver.class); 
	
	// Role-related fields
	@UiField DualGridPanel<Role> rolesPanel;
	
	private User currentUser;
	
	// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	public UsersPanel() {
		// Global Handlers -- should be registered only once
		BaseModule.addHandler(UserCreatedEvent.TYPE, new CreationHandler<User>() {
			@Override
			public void onCreated(CreatedEvent<User> e) {
				grid.getStore().add(e.getObject());
			}
		});
		
		setWidget(binder.createAndBindUi(this));
		
		// Bind the filter into the grid's store
		gridFilter.bind(grid.getStore());
		
		// Initialize the driver, used to run the form editor
		driver.initialize(this);
		
		// Validators
		username.addValidator(CoreValidators.USERNAME_VALIDATOR);
		email.addValidator(CoreValidators.EMAIL_VALIDATOR);
		firstName.addValidator(CoreValidators.NAME_VALIDATOR);
		middleName.addValidator(CoreValidators.MIDDLE_NAME_VALIDATOR);
		lastName.addValidator(CoreValidators.NAME_VALIDATOR);
	}
	
	// UiFactory Methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@Ignore
	@UiFactory
	StoreFilterField<User> initializeGridFilter() {
		StoreFilterField<User> f = new StoreFilterField<User>() {
			@Override
			protected boolean doSelect(Store<User> store, User parent, User item, String filter) {
				String filterInLowerCase = filter.toLowerCase().trim();
				return item.getUsername().contains(filterInLowerCase);
			}
		};
		return f;
	}
	
	/**
	 * Used to create the 'grid' field in the XML file. 
	 * 
	 * @return
	 */
	@UiFactory
	Grid<User> initializeGrid() {
		UserProperties props = UserProperties.INSTANCE;

		List<ColumnConfig<User, ?>> columns = new ArrayList<ColumnConfig<User, ?>>();

		ColumnConfig<User, String> username = UI.column(props.username(), 100, "Username", new AbstractCell<String>() {
			@Override
			public void render(Context context, String value, SafeHtmlBuilder sb) {
				sb.append(SafeHtmlUtils.fromTrustedString("<b>" + value + "</b>"));
			}
		});
		columns.add(username);

		columns.add(UI.column(props.inverseName(), 200, "Full Name"));
		columns.add(UI.column(props.email(), 200, "Email"));

		AbstractCell<Timestamp> dateCell = new AbstractCell<Timestamp>() {
			@Override
			public void render(Context context, Timestamp timestamp, SafeHtmlBuilder sb) {
				sb.appendHtmlConstant(DateUtils.getUserFriendlyDate(timestamp));
			}
		};
		
		columns.add(UI.column(props.dateCreated(), 150, "Date Created", dateCell));
		columns.add(UI.column(props.lastLogin(), 150, "Last Login", dateCell));
		columns.add(UI.column(props.status(), 100, "Status"));
		
		ColumnModel<User> cm = new ColumnModel<User>(columns);
		
		GridView<User> view = new BasicGridView<User>();
		view.setEmptyText("There are no users to display.");
		view.setAutoExpandColumn(username);

		final Grid<User> grid = new Grid<User>(new ListStore<User>(props.key()), cm, view);
		grid.setAllowTextSelection(false);
		grid.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
		
		// Add a listener to the grid
		grid.getSelectionModel().addSelectionChangedHandler(new SelectionChangedHandler<User>() {
			@Override
			public void onSelectionChanged(SelectionChangedEvent<User> event) {
				final User user = grid.getSelectionModel().getSelectedItem();
				if (user == null)
					return;
				
				if (driver.isDirty()) { // If there are unsaved changes in the form
					UI.confirm("Unsaved Changes", "There are unsaved changes. Would you like to continue loading?",
						new ConfirmDialogHandler() {
							@Override
							public void onYes(HideEvent event) {
								edit(user);
							}
						}
					);
				} else {
					edit(user);
				}
			}
		});
		
		return grid;
	}
	
	@UiFactory
	HtmlLayoutContainer initializeFormContainer() {
		return new HtmlLayoutContainer(getFormLayout());
	}
	
	@UiFactory
	DualGridPanel<Role> initializeRolesPanel() {
		DualGridPanel<Role> panel = new DualGridPanel<Role>() {
			@Override
			protected Grid<Role> createLeftGrid() {
				SimpleRoleProperties props = GWT.create(SimpleRoleProperties.class);
				List<ColumnConfig<Role, ?>> columns = new ArrayList<ColumnConfig<Role, ?>>();
				ColumnConfig<Role, String> name = UI.column(props.name(), 200, "Name"); 
				columns.add(name);
				
				Grid<Role> grid = new Grid<Role>(new ListStore<Role>(props.key()), new ColumnModel<Role>(columns));
				grid.setAllowTextSelection(false);
				grid.setHeight(200);
				grid.getSelectionModel().setSelectionMode(SelectionMode.MULTI);
				grid.setBorders(true);
				
				GridView<Role> view = new BasicGridView<Role>();
				view.setEmptyText("There are no roles to display.");
				view.setAutoExpandColumn(name);
				grid.setView(view);
				
				return grid;
			}

			@Override
			protected Grid<Role> createRightGrid() {
				SimpleRoleProperties props = GWT.create(SimpleRoleProperties.class);
				List<ColumnConfig<Role, ?>> columns = new ArrayList<ColumnConfig<Role, ?>>();
				ColumnConfig<Role, String> name = UI.column(props.name(), 200, "Name"); 
				columns.add(name);
				
				Grid<Role> grid = new Grid<Role>(new ListStore<Role>(props.key()), new ColumnModel<Role>(columns));
				grid.setAllowTextSelection(false);
				grid.setHeight(200);
				grid.getSelectionModel().setSelectionMode(SelectionMode.MULTI);
				grid.setBorders(true);
				
				GridView<Role> view = new BasicGridView<Role>();
				view.setEmptyText("User does not have any roles yet.");
				view.setAutoExpandColumn(name);
				grid.setView(view);
				
				return grid;
			}

			@Override
			protected void onLeftToRight(List<Role> list) {
				assignRoles(list, true);
			}

			@Override
			protected void onRightToLeft(List<Role> list) {
				unassignRoles(list, true);
			}

			@Override
			protected void allRightClicked(SelectEvent e) {
				// XXX We have to wrap into an ArrayList as the return value of #getAll()
				// which is a different type of List cannot be serialized by GWT. Weird.
				assignRoles(new ArrayList<Role>(rolesPanel.getLeftGrid().getStore().getAll()), false);
			}
			
			@Override
			protected void rightClicked(SelectEvent e) {
				assignRoles(rolesPanel.getLeftGrid().getSelectionModel().getSelectedItems(), false);
			}
			
			@Override
			protected void leftClicked(SelectEvent e) {
				unassignRoles(rolesPanel.getRightGrid().getSelectionModel().getSelectedItems(), false);
			}
			
			@Override
			protected void allLeftClicked(SelectEvent e) {
				// XXX We have to wrap into an ArrayList as the return value of #getAll()
				// which is a different type of List cannot be serialized by GWT. Weird.
				unassignRoles(new ArrayList<Role>(rolesPanel.getRightGrid().getStore().getAll()), false);
			}
			
		};
		
		panel.setLeftGridLabel("The list of Roles in the system.");
		panel.setRightGridLabel("The list of Roles the user has.");
		
		return panel;
	}
	
	/**
	 * Used to provide the instances for HtmlData fields in the XML file
	 * 
	 * @return
	 */
	@UiFactory
	HtmlData createHtmlData() {
		return new HtmlData("");
	}
	
	// Logic %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	/**
	 * Loads <tt>user</tt> into the form editor.
	 * 
	 * @param user
	 */
	private void edit(final User user) {
		currentUser = user;
		
		driver.edit(user);
		
		saveBtn.setEnabled(user != null);
		cancelBtn.setEnabled(user != null);
		activateBtn.setEnabled(user != null);
		
		rolesPanel.clearBoth();

		if (user == null)
			return;
		
		UserStatus status = user.getStatus();
		if (status == UserStatus.ACTIVE) {
			activateBtn.setText("Deactivate User");
			activateBtn.setIcon(CoreResources.INSTANCE.deactivate());
		} else {
			activateBtn.setText("Activate User");
			activateBtn.setIcon(CoreResources.INSTANCE.activate());
		}
		
		// Load the Companies into the grid
		companiesGrid.getStore().clear();
		companiesGrid.getStore().addAll(user.getCompanies());
		
		// Load the Roles into the grids
		UI.mask(rolesPanel, "Loading roles...");
		roleSvc().getAll(new Callback<List<Role>>(rolesPanel) {
			@Override
			protected void handleSuccess(List<Role> roles) {
				rolesPanel.clearBoth();
				
				// Remove those roles user already has
				roles.removeAll(user.getRoles());
				
				rolesPanel.getLeftGrid().getStore().addAll(roles);
				rolesPanel.getRightGrid().getStore().addAll(user.getRoles());
			}
		});
	}
	
	private void assignRoles(final List<Role> roles, final boolean viaDND) {
		if (roles.isEmpty())
			return;
		
		final User user = driver.flush();
		if (user == null)
			return;
		
		UI.mask(mainPanel, "Assigning roles to user...");
//		DispatchAsync dispatch = new StandardDispatchAsync(new DefaultExceptionHandler());
//		dispatch.execute(new AssignRolesToUserAction(user, roles), new Callback<AssignRolesToUserResult>(mainPanel) {
		userSvc().assignRoles(user, roles, new Callback<List<Role>>(mainPanel) {
			@Override
			protected void handleSuccess(List<Role> result) {
				user.addRoles(result);
				grid.getStore().update(user);
				
				if (viaDND) return;
				// If assignRoles was not called via DND, add the roles to rolesPanel.getRightGrid()
				for (Role role : result) {
					rolesPanel.getLeftGrid().getStore().remove(role);
					rolesPanel.getRightGrid().getStore().add(role);
				}
			}
			
			@Override
			protected void handleFailure(Throwable t) {
				if (!viaDND) return;
				// If assignRoles was called via DND, return the roles back to rolesPanel.getLeftGrid()
				for (Role role : roles) {
					rolesPanel.getRightGrid().getStore().remove(role);
					rolesPanel.getLeftGrid().getStore().add(role);
				}
			}
		});
	}
	
	private void unassignRoles(final List<Role> roles, final boolean viaDND) {
		if (roles.isEmpty())
			return;
		
		final User user = driver.flush();
		if (user == null)
			return;
		
		UI.mask(mainPanel, "Unassigning roles from user...");
		userSvc().unassignRoles(user, roles, new Callback<List<Role>>(mainPanel) {
			@Override
			protected void handleSuccess(List<Role> result) {
				user.removeRoles(result);
				grid.getStore().update(user);
				
				if (viaDND) return;
				// If unassignRoles was not called via DND, add the roles to the rolesPanel.getLeftGrid()
				for (Role role : result) {
					rolesPanel.getRightGrid().getStore().remove(role);
					rolesPanel.getLeftGrid().getStore().add(role);
				}
			}
			
			@Override
			protected void handleFailure(Throwable t) {
				if (!viaDND) return;
				// If unassignRoles was called via DND, return the roles back to rolesPanel.getRightGrid()
				for (Role role : roles) {
					rolesPanel.getLeftGrid().getStore().remove(role);
					rolesPanel.getRightGrid().getStore().add(role);
				}
			}
		});
	}
	
	// Listeners %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	// Called when the 'Add' button is clicked
	@UiHandler("addBtn")
	void addClicked(SelectEvent e) {
		BaseModule.visit(AdminTasks.ADD_USER);
	}
	
	@UiHandler("moreBtnMenu")
	void onMenuSelection(SelectionEvent<Item> e) {
		Item item = e.getSelectedItem();
		if (item == refreshMenuItem) {
			refreshClicked();
		} else if (item == deleteMenuItem) {
			deleteClicked();
		} 
	}
	
	// Called when the 'Refresh' button is clicked
	void refreshClicked() {
		loadData();
	}
	
	void deleteClicked() {
		final User selected = grid.getSelectionModel().getSelectedItem();
		if (selected == null)
			return;
		
		UI.confirm("Confirm Delete", "Are you sure you want to delete '" + selected.getUsername() + "'?",
			new ConfirmDialogHandler() {
				@Override
				public void onYes(HideEvent event) {
					UI.mask(mainPanel, "Deleting " + selected.getUsername() + "...");
					userSvc().delete(selected, new Callback<User>(mainPanel) {
						@Override
						protected void handleSuccess(User result) {
							result.setPassword(null);
							grid.getStore().remove(result);
							
							// Clear the form editor if necessary
							User inEditor = driver.flush();
							if (inEditor != null && inEditor.getUsername().equals(result.getUsername()))
								edit(null);
						}
					});
				}
			}
		);
	}
	
	// Called when the 'Activate' button is clicked
	@UiHandler("activateBtn")
	void activateClicked(SelectEvent e) {
		User user = driver.flush();
		if (user == null)
			return;
		
		Callback<User> callback = new Callback<User>(mainPanel) {
			@Override
			protected void handleSuccess(User result) {
				grid.getStore().update(result);
				edit(result);
			}
		};
		
		if (user.getStatus() == UserStatus.ACTIVE) {
			UI.mask(mainPanel, "Deactivating user...");
			userSvc().deactivate(user, callback);
		} else {
			UI.mask(mainPanel, "Activating user...");
			userSvc().activate(user, callback);
		}
	}
	
	// Called when the 'Save' button is clicked
	@UiHandler("saveBtn")
	void saveClicked(SelectEvent e) {
		User user = driver.flush();
		if (driver.hasErrors())
			return;
		
		UI.mask(mainPanel, "Updating user...");
		userSvc().update(user, new Callback<User>(mainPanel) {
			@Override
			protected void handleSuccess(User result) {
				result.setPassword(null);
				grid.getStore().update(result);
				edit(result);
			}
		});
	}
	
	// Called when the 'Cancel' button is clicked
	@UiHandler("cancelBtn")
	void cancelClicked(SelectEvent e) {
		edit(null);
	}
	
	@UiHandler("assignCompanyBtn")
	void assignCompanyClicked(SelectEvent e) {
		if (currentUser == null)
			return;
		
		final CompanyListWindow cw = new CompanyListWindow();
		final Window w = (Window) cw.asWidget();
		w.addHideHandler(new HideHandler() {
			@Override
			public void onHide(HideEvent event) {
				if (w.getHideButton() != null && w.getHideButton().getText().equals("OK")) {
					List<Company> companies = cw.getSelected();
					if (companies.isEmpty())
						return;
					
					mainPanel.mask("Assigning companies...");
					AdminServices.userSvc().assignCompanies(currentUser, companies, new Callback<List<Company>>(mainPanel) {
						@Override
						protected void handleSuccess(List<Company> result) {
							companiesGrid.getStore().addAll(result);
							
							currentUser.addCompanies(result);
							grid.getStore().update(currentUser);
						}
					});
				}
			}
		});
		w.show();
		w.center();
		cw.loadData(companiesGrid.getStore().getAll());
	}
	
	@UiHandler("unassignCompanyBtn")
	void unassignCompanyClicked(SelectEvent e) {
		if (currentUser == null)
			return;
		
		List<Company> companies = companiesGrid.getSelectionModel().getSelectedItems();
		if (companies.isEmpty())
			return;
		
		mainPanel.mask("Unassigning companies...");
		AdminServices.userSvc().unassignCompanies(currentUser, companies, new Callback<List<Company>>(mainPanel) {
			@Override
			protected void handleSuccess(List<Company> result) {
				for (Company company : result) {
					companiesGrid.getStore().remove(company);
					currentUser.removeCompany(company);
				}
						
				grid.getStore().update(currentUser);
			}
		});
	}
	 
	// ITabItemPanel Methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@Override
	public String getPanelId() {
		return AdminTasks.USERS.getId();
	}
	
	@Override
	public TabItemConfig getConfig() {
		if (config == null) {
			config = new TabItemConfig("Users", true);
			config.setIcon(AdminResources.INSTANCE.users());
		}
		return config;
	}
	
	@Override
	public void loadData() {
		// Load data
		Log.debug("Loading users...");
		
		UI.mask(mainPanel, "Loading users...");
		userSvc().getAll(new Callback<List<User>>(mainPanel) {
			@Override
			protected void handleSuccess(List<User> result) {
				grid.getStore().clear();
				grid.getStore().addAll(result);

				edit(null); // Clear the form
			}
		});
	}
	
	// Helper Methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	private static native String getFormLayout() /*-{
	    return [ 
	    '<table width=100% cellpadding=0 cellspacing=0>',
		    '<tr><td class=uname width=33%></td><td class=email width=33%></td><td class=passwd width=33%></td></tr>',
		    '<tr><td class=fname width=33%></td><td class=mname width=33%></td><td class=lname width=33%></td></tr>',
	    '</table>'
		].join("");
	}-*/;

	// Classes %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	interface SimpleRoleProperties extends PropertyAccess<Role> {
		@Path("name")
		ModelKeyProvider<Role> key();
		ValueProvider<Role, String> name();
	}
	
}
